package org.xmx0632.flywayside;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/*
 * Copyright 2001-2005 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.StringUtils;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.internal.info.MigrationInfoDumper;
import org.xmx0632.flywayside.constant.Constant;
import org.xmx0632.flywayside.constant.ModelEnum;
import org.xmx0632.flywayside.handler.BaseHandler;
import org.xmx0632.flywayside.param.FlywayParam;
import org.xmx0632.flywayside.param.FlywaySideParams;
import org.xmx0632.flywayside.utils.GlobalTableReplaceHolderMap;
import org.xmx0632.flywayside.utils.Helper;

import com.google.inject.internal.util.Lists;
import com.google.inject.internal.util.Maps;

/**
 * 
 * @author xmx0632
 *
 */
public abstract class BaseFlywaySideMojo extends AbstractMojo {

	// 默认不分库分表
	@Parameter(defaultValue = "MODEL1")
	protected ModelEnum modelEnum;

	@Parameter(defaultValue = "filesystem:src/main/resources/db/migration")
	protected String location;

	// instance
	@Parameter(defaultValue = "1")
	protected Integer instanceNumber;

	@Parameter
	protected List<String> instanceList = new ArrayList<String>();

	// db
	@Parameter(defaultValue = "db_")
	protected String dbPrefix;

	@Parameter(defaultValue = "1")
	protected Integer dbNumber;

	@Parameter
	protected List<String> dbList = new ArrayList<String>();

	// table
	@Parameter(defaultValue = "1")
	protected Integer tableNumberPerDb;

	@Parameter(defaultValue = "1")
	protected Integer allTableNumber;

	// 1:不填充；>1：用0填充左边空位
	@Parameter(defaultValue = "3")
	private Integer tablePaddingNumber = 3;

	// 表名映射关系
	@Parameter
	protected List<String> tableMappings = new ArrayList<String>();

	/**
	 * 根据参数获取不同模式的实现
	 * 
	 * @param flywaySideParams
	 *            调用flyway api使用的参数
	 * @return 根据不同模式返回具体实现类
	 */
	protected abstract BaseHandler getHandler(FlywaySideParams flywaySideParams);

	@Override
	public void execute() throws MojoExecutionException {

		printAgrs();

		initTableMapping();

		FlywaySideParams flywaySideParams = new FlywaySideParams(allTableNumber, instanceNumber, instanceList, dbPrefix,
				dbNumber, dbList, tableNumberPerDb, location, tablePaddingNumber, tableMappings);

		BaseHandler handler = getHandler(flywaySideParams);
		if (handler == null) {
			getLog().error("handler not found for " + flywaySideParams);
			throw new MojoExecutionException("handler not found!");
		}

		List<FlywayParam> flywayParams = handler.handle();

		executeFlywayCommand(flywayParams);
	}

	/**
	 * fill map with config item according modelEnum
	 * 
	 * <pre>
		tableNamePrefixPlaceHolderInSQL|realTableNamePrefix|indexRange[start-end]
		tableName|tbl_test_|0-3
		tableName|tbl_test_ == tableName|tbl_test_|0
		tableName|tbl_test_ == tableName|tbl_test_|0-3
		
		
		create table ${tableName0} (
		    ID int not null,
		    NAME varchar(100) not null
		);
		...
		create table ${tableName3} (
		    ID int not null,
		    NAME varchar(100) not null
		);
		
		will be replaced as :
		
		create table tbl_test_000 (
		    ID int not null,
		    NAME varchar(100) not null
		);
		...
		create table tbl_test_003 (
		    ID int not null,
		    NAME varchar(100) not null
		);
	 * </pre>
	 * 
	 * @return
	 * @throws MojoExecutionException
	 */
	protected Map<String, Map<String, String>> initTableMapping() throws MojoExecutionException {
		Map<String, Map<String, String>> placeHoldersMap = new HashMap<String, Map<String, String>>(16);
		if (tableMappings == null || tableMappings.size() == 0) {
			getLog().info("tableMappings is empty.placeHolderReplacement will not be supported.");
			return placeHoldersMap;
		}

		for (String tableConfig : tableMappings) {
			getLog().info("tableConfig:" + tableConfig);
			if (StringUtils.isBlank(tableConfig)) {
				getLog().debug("skip empty config");
				continue;
			}

			String[] tableAndIndexs = StringUtils.split(tableConfig, "|");
			if (tableAndIndexs.length < 2) {
				getLog().error("invalid tableConfig:" + tableConfig + "!");
				throw new MojoExecutionException("invalid tableConfig:" + tableConfig + "!");
			}
			String tablePrefixInSql = tableAndIndexs[0];
			String tablePrefixReplacement = tableAndIndexs[1];

			int rangeLength = tableAndIndexs.length - 2;
			getLog().info("rangeLength:" + rangeLength);

			String[] tableIndexRanges = getTableIndexRanges(tableAndIndexs, rangeLength);

			if (!isValidRanage(tableIndexRanges, allTableNumber)) {
				getLog().error("tableIndexRanges and allTableNumber not match");
				throw new MojoExecutionException("tableIndexRanges and allTableNumber not match");
			}

			for (String tableIndexRange : tableIndexRanges) {
				getLog().debug("tablePrefixInSql:" + tablePrefixInSql + ", tablePrefixReplacement:"
						+ tablePrefixReplacement + ", tableIndexRange:" + tableIndexRange);
				if (StringUtils.isBlank(tablePrefixReplacement)) {
					getLog().error("invalid tablePrefix:" + tablePrefixReplacement);
					throw new MojoExecutionException("invalid tablePrefix");
				}
				if (StringUtils.isBlank(tableIndexRange)) {
					getLog().error("invalid tableIndexRange:" + tableIndexRange);
					throw new MojoExecutionException("invalid tableIndexRange");
				}

				String[] ranges = StringUtils.split(tableIndexRange, "-");
				int startIndex = Integer.valueOf(ranges[0]);
				int endIndex = Integer.valueOf(ranges[1]);
				getLog().info("startIndex:" + startIndex + " ,endIndex:" + endIndex);

				for (int instIndex = 0; instIndex < instanceNumber; instIndex++) {
					for (int dbIndex = 0; dbIndex < dbNumber; dbIndex++) {

						int dbIndex1 = dbIndex;
						if (modelEnum == ModelEnum.MODEL1 || modelEnum == ModelEnum.MIX1) {
							dbIndex1 = instIndex * dbNumber + dbIndex;
						}
						String dbKey = GlobalTableReplaceHolderMap.getKey(instIndex, dbIndex1);
						getLog().debug("dbKey:" + dbKey);
						Map<String, String> tableMap = createMapIfNotExist(placeHoldersMap, dbKey);
						if (modelEnum == ModelEnum.MODEL2 || modelEnum == ModelEnum.MIX1) {
							// reset startIndex
							startIndex = Integer.valueOf(ranges[0]);
						}
						for (int j = 0; j < tableNumberPerDb; j++) {
							String tableIdx = Helper.formatPaddingIndex(startIndex++, tablePaddingNumber);
							getLog().debug(">>> put " + tablePrefixInSql + j + "=" + tablePrefixReplacement + tableIdx);

							tableMap.put(tablePrefixInSql + j, tablePrefixReplacement + tableIdx);
						}
					}
				}
			}

		}
		GlobalTableReplaceHolderMap.Instance.getMap().putAll(placeHoldersMap);
		return placeHoldersMap;
	}

	private Map<String, String> createMapIfNotExist(Map<String, Map<String, String>> placeHoldersMap, String dbKey) {
		Map<String, String> tableMap = placeHoldersMap.get(dbKey);
		if (tableMap == null) {
			tableMap = Maps.newHashMap();
			placeHoldersMap.put(dbKey, tableMap);
		}
		return tableMap;
	}

	/**
	 * 获取table范围
	 * 
	 * @param tableAndIndexs
	 * @param rangeLength
	 * @return
	 */
	private String[] getTableIndexRanges(String[] tableAndIndexs, int rangeLength) {
		String[] tableIndexRanges = null;
		if (rangeLength == 0) {
			// 根据不同modelEnum生成对应的range表达式
			if (ModelEnum.MODEL1 == modelEnum || ModelEnum.MIX2 == modelEnum) {
				tableIndexRanges = new String[] { "0-" + (allTableNumber - 1) };
				getLog().info(modelEnum + ", tableIndexRanges size:" + tableIndexRanges.length);
			}
			if (ModelEnum.MODEL2 == modelEnum || ModelEnum.MIX1 == modelEnum) {
				List<String> list = Lists.newArrayList();
				int dbNumbers = dbNumber * instanceNumber;
				for (int i = 0; i < dbNumbers; i++) {
					list.add("0-" + (tableNumberPerDb - 1));
				}
				tableIndexRanges = list.toArray(new String[list.size()]);
				getLog().info(modelEnum + " tableIndexRanges size:" + tableIndexRanges.length);
			}
		} else {
			tableIndexRanges = new String[rangeLength];
			getLog().info(modelEnum + " tableIndexRanges size:" + tableIndexRanges.length);
			System.arraycopy(tableAndIndexs, 2, tableIndexRanges, 0, rangeLength);
		}
		return tableIndexRanges;
	}

	/**
	 * TODO 验证tableIndexRanges中总数累加是否正确(==allTableNumber),用于支持不连续的分段区间
	 * 
	 * @param tableIndexRanges
	 * @param allTableNumber2
	 * @return
	 */
	private boolean isValidRanage(String[] tableIndexRanges, Integer allTableNumber) {
		return true;
	}

	protected void executeFlywayCommand(List<FlywayParam> flywayParams) {
		List<String> executedDbList = new ArrayList<String>();

		for (int i = 0; i < flywayParams.size(); i++) {
			FlywayParam param = flywayParams.get(i);

			String key = param.toString();
			if (executedDbList.contains(key)) {
				getLog().debug("skip executed schema:" + key);
				continue;
			}

			executedDbList.add(key);

			getLog().info(Constant.SPLITTER + "exec " + param.getCommand() + ":" + param);

			Flyway flyway = getFlyway(param);

			if (FlywayParam.COMMAND_MIGRATE.equals(param.getCommand())) {
				flyway.migrate();
			}

			if (FlywayParam.COMMAND_INFO.equals(param.getCommand())) {
				String info = MigrationInfoDumper.dumpToAsciiTable(flyway.info().all());
				getLog().info("\n" + info);
			}

			if (FlywayParam.COMMAND_VALIDATE.equals(param.getCommand())) {
				flyway.validate();
			}

			if (FlywayParam.COMMAND_REPAIR.equals(param.getCommand())) {
				flyway.repair();
			}

			if (FlywayParam.COMMAND_BASELINE.equals(param.getCommand())) {
				flyway.baseline();
			}

		}
	}

	private Flyway getFlyway(FlywayParam param) {
		Flyway flyway = new Flyway();
		flyway.setLocations(param.getLocations());
		flyway.setSqlMigrationPrefix("V");
		flyway.setSqlMigrationSuffix(".sql");
		flyway.setSchemas(param.getDbName());
		flyway.setDataSource(param.getUrl(), param.getUser(), param.getPassword());

		if (param.getPlaceHoldersMap() != null) {
			flyway.setPlaceholderReplacement(true);
			flyway.getPlaceholders().putAll(param.getPlaceHoldersMap());
		} else {
			flyway.setPlaceholderReplacement(false);
		}
		return flyway;
	}

	protected void printAgrs() {
		getLog().info("*********** summary ***********");
		getLog().info("modelEnum:" + modelEnum);
		getLog().info("--------------------------------");
		getLog().info("instanceNumber:" + instanceNumber);
		getLog().info("instanceList:" + instanceList);
		getLog().info("--------------------------------");
		getLog().info("dbPrefix:" + dbPrefix);
		getLog().info("dbNumber:" + dbNumber);
		getLog().info("dbList:" + dbList);
		getLog().info("--------------------------------");
		getLog().info("tableNumberPerDb:" + tableNumberPerDb);
		getLog().info("********************************");

		getLog().info("allTableNumber:" + allTableNumber);
		getLog().info("tableNumberPerDb:" + tableNumberPerDb);
	}

	public void setModelEnum(ModelEnum modelEnum) {
		this.modelEnum = modelEnum;
	}

	public void setLocation(String location) {
		this.location = location;
	}

	public void setInstanceNumber(Integer instanceNumber) {
		this.instanceNumber = instanceNumber;
	}

	public void setInstanceList(List<String> instanceList) {
		this.instanceList = instanceList;
	}

	public void setDbPrefix(String dbPrefix) {
		this.dbPrefix = dbPrefix;
	}

	public void setDbNumber(Integer dbNumber) {
		this.dbNumber = dbNumber;
	}

	public void setDbList(List<String> dbList) {
		this.dbList = dbList;
	}

	public void setTableNumberPerDb(Integer tableNumberPerDb) {
		this.tableNumberPerDb = tableNumberPerDb;
	}

	public void setAllTableNumber(Integer allTableNumber) {
		this.allTableNumber = allTableNumber;
	}

	public void setTablePaddingNumber(Integer tablePaddingNumber) {
		this.tablePaddingNumber = tablePaddingNumber;
	}

	public void setTableMappings(List<String> tableMappings) {
		this.tableMappings = tableMappings;
	}

}
